/**
 * @fileoverview StreamIgnorer is an XPCOM component that rewrites
 * responses to requests to particular URLs. It implements nsIStreamListener,
 * but pays no attention to the content of the original request.
 * @author bolinfest@gmail.com (Michael Bolin)
 */

// Cannot use Components.utils.import() as written in the sample documentation
// because "import" is a reserved word, so it is rejected by the Closure Compiler.
Components.utils['import']('resource://gre/modules/XPCOMUtils.jsm');


/**
 * @constructor
 * @implements {nsIStreamListener}
 */
var StreamIgnorer = function() {
  this.wrappedJSObject = this;
};


/**
 * @type {Object.<string>}
 */
StreamIgnorer.prototype.jsLibraryUrlToInstrumentedFile = {
  // Currently, requesting version 1.3 of jQuery returns version 1.3.2.
  'ajax.googleapis.com/ajax/libs/jquery/1.3/jquery.min.js':
      'jquery-1.3.2.js',
  'ajax.googleapis.com/ajax/libs/jquery/1.3/jquery.js':
      'jquery-1.3.2.js',

  'ajax.googleapis.com/ajax/libs/jquery/1.3.0/jquery.min.js':
      'jquery-1.3.0.js',
  'ajax.googleapis.com/ajax/libs/jquery/1.3.0/jquery.js':
      'jquery-1.3.0.js',
  'ajax.googleapis.com/ajax/libs/jquery/1.3.1/jquery.min.js':
      'jquery-1.3.1.js',
  'ajax.googleapis.com/ajax/libs/jquery/1.3.1/jquery.js':
      'jquery-1.3.1.js',
  'ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js':
      'jquery-1.3.2.js',
  'ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.js':
      'jquery-1.3.2.js',

  // Currently, requesting version 1.4 of jQuery returns version 1.4.4.
  'ajax.googleapis.com/ajax/libs/jquery/1.4/jquery.min.js':
      'jquery-1.4.4.js',
  'ajax.googleapis.com/ajax/libs/jquery/1.4/jquery.js':
      'jquery-1.4.4.js',

  'ajax.googleapis.com/ajax/libs/jquery/1.4.0/jquery.min.js':
      'jquery-1.4.0.js',
  'ajax.googleapis.com/ajax/libs/jquery/1.4.0/jquery.js':
      'jquery-1.4.0.js',
  'ajax.googleapis.com/ajax/libs/jquery/1.4.1/jquery.min.js':
      'jquery-1.4.1.js',
  'ajax.googleapis.com/ajax/libs/jquery/1.4.1/jquery.js':
      'jquery-1.4.1.js',
  'ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js':
      'jquery-1.4.2.js',
  'ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.js':
      'jquery-1.4.2.js',
  'ajax.googleapis.com/ajax/libs/jquery/1.4.3/jquery.min.js':
      'jquery-1.4.3.js',
  'ajax.googleapis.com/ajax/libs/jquery/1.4.3/jquery.js':
      'jquery-1.4.3.js',
  'ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js':
      'jquery-1.4.4.js',
  'ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.js':
      'jquery-1.4.4.js',

  // Currently, requesting version 1.5 of jQuery returns version 1.5.2.
  'ajax.googleapis.com/ajax/libs/jquery/1.5/jquery.min.js':
      'jquery-1.5.2.js',
  'ajax.googleapis.com/ajax/libs/jquery/1.5/jquery.js':
      'jquery-1.5.2.js',

  'ajax.googleapis.com/ajax/libs/jquery/1.5.0/jquery.min.js':
      'jquery-1.5.0.js',
  'ajax.googleapis.com/ajax/libs/jquery/1.5.0/jquery.js':
      'jquery-1.5.0.js',
  'ajax.googleapis.com/ajax/libs/jquery/1.5.1/jquery.min.js':
      'jquery-1.5.1.js',
  'ajax.googleapis.com/ajax/libs/jquery/1.5.1/jquery.js':
      'jquery-1.5.1.js',
  'ajax.googleapis.com/ajax/libs/jquery/1.5.2/jquery.min.js':
      'jquery-1.5.2.js',
  'ajax.googleapis.com/ajax/libs/jquery/1.5.2/jquery.js':
      'jquery-1.5.2.js',
};


/**
 * This is a map that should have the same structure as
 * jsLibraryUrlToInstrumentedFile. Its value comes from the
 * extensions.jsbloat.overrides preference, which the user can use to add his or
 * her own URLs that should be replaced with an instrumented library. For
 * example, the user might add the following:
 * 
 * {
 *   "mozcom-cdn.mozilla.net/js/jquery/jquery.min.js": "jquery-1.4.4.js"
 * }
 *
 * In order to test: http://www.mozilla.com/en-US/firefox/new/?from=getfirefox
 *
 * @type {Object.<string>}
 */
StreamIgnorer.prototype.userUrlMapping = {};


/**
 * @param {string} url
 * @return {string|undefined}
 */
StreamIgnorer.prototype.getInstrumentedFileForUrl = function(url) {
  var delimiter = '://';
  var index = url.indexOf(delimiter);
  if (index < 0) {
    return undefined;
  }

  url = url.substring(index + delimiter.length);
  return this.userUrlMapping[url] || this.jsLibraryUrlToInstrumentedFile[url];
};


/** @type {nsIStreamListener} */
StreamIgnorer.prototype.listener = null;


/** @type {string} */
StreamIgnorer.prototype.classDescription =
    'StreamIgnorer XPCOM Component';


/** @type {nsJSID} */
StreamIgnorer.prototype.classID = Components.ID(
    '{5478c86b-1969-4620-9851-8294e8f83666}');


/** @type {string} */
StreamIgnorer.prototype.contractID =
    '@jsbloat.googlecode.com/StreamIgnorer/;1';


/** @type {Function} */
StreamIgnorer.prototype.QueryInterface =
    XPCOMUtils.generateQI([
      Components.interfaces.nsISupports,
      Components.interfaces.nsIStreamListener,
      Components.interfaces.nsIRequestObserver
    ]);


/**
 * Defined in the nsIRequestObserver interface.
 * @param {nsIRequest} request
 * @param {nsISupports} context
 * @override
 */
StreamIgnorer.prototype.onStartRequest = function(request, context) {
  request.QueryInterface(Components.interfaces.nsIChannel);
  var channel = /** @type {nsIChannel} */ (request);

  // Load the chrome:// URL that contains the instrumented JavaScript for the
  // requested URL.
  var url = channel.URI.spec;
  var chromeUrl = 'chrome://jsbloat/content/' +
      this.getInstrumentedFileForUrl(url);

  var ioService = Components.classes['@mozilla.org/network/io-service;1']
      .getService(Components.interfaces.nsIIOService);
  var chromeChannel = ioService.newChannel(chromeUrl, null, null);

  var listener = this.listener;
  var streamListener = {
      onStartRequest: function(req, ctx) {
        listener.onStartRequest(request, context);
      },
      onDataAvailable: function(req, ctx, inputStream, offset, count) {
        listener.onDataAvailable(request, context, inputStream, offset, count);
      },
      onStopRequest: function(req, ctx, statusCode) {
        listener.onStopRequest(request, context, statusCode);
      }
  };
  chromeChannel.asyncOpen(streamListener, {} /* context */);
};


/**
 * Defined in the nsIRequestObserver interface.
 * @param {nsIRequest} request
 * @param {nsISupports} context
 * @param {nsresult} statusCode
 * @override
 */
StreamIgnorer.prototype.onStopRequest = function(
    request, context, statusCode) {
};


/**
 * Defined in the nsIStreamListener interface.
 * @param {nsIRequest} request
 * @param {nsISupports} context
 * @param {nsIInputStream} inputStream
 * @param {number} offset
 * @param {number} count
 * @override
 */
StreamIgnorer.prototype.onDataAvailable = function(
    request, context, inputStream, offset, count) {
};

// Set up a preference observer in order to keep
// StreamIgnorer.prototype.userUrlMapping up to date.
//
// This is modeled after:
// https://developer.mozilla.org/en/Code_snippets/Preferences
var prefService = Components.classes['@mozilla.org/preferences-service;1'].
    getService(Components.interfaces.nsIPrefService);
var myPrefObserver = {
  register: function() {
    this.branch_ = prefService.getBranch('extensions.jsbloat.');
    this.branch_.QueryInterface(Components.interfaces.nsIPrefBranch2);
    this.branch_.addObserver('', this, false);
  },

  unregister: function() {
    if (!this.branch_) return;
    this.branch_.removeObserver('', this);
  },
 
  observe: function(subject, topic, data) {
    if (topic != 'nsPref:changed') return;
    if (data == 'overrides') {
      this.update();
    }
  },

  update: function() {
    try {
      var json = this.branch_.getCharPref('overrides');
      var map = JSON.parse(json);
      var urlMap = {};
      for (var key in map) {
        var value = map[key];
        if (typeof key == 'string' && typeof value == 'string') {
          urlMap[key] = value;
        }
      }
      StreamIgnorer.prototype.userUrlMapping = urlMap;
    } catch (e) {
      // Oh well!
      dump('Error parsing extensions.jsbloat.overrides: ' + e + '\n');
    }
  }
};
myPrefObserver.register();
myPrefObserver.update();


var NSGetFactory, NSGetModule;
if (XPCOMUtils.generateNSGetFactory) {
  // Firefox 4.0 and later.
  NSGetFactory = XPCOMUtils.generateNSGetFactory([StreamIgnorer]);
} else {
  // Firefox 3.0-3.6.
  NSGetModule = XPCOMUtils.generateNSGetModule([StreamIgnorer]);
}
